package llama.core;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import llama.config.IConfigurator;
import llama.network.IServerConnection;
import llama.network.NetworkMessage;
import llama.util.SystemMessage;

public class ChatModel implements IChatModel {
	private PropertyChangeSupport pcs;
	private IServerConnection serverConn;
	private IConfigurator configurator;
	
	// Variables to hold information of the active user currently logged in.
	private IUser activeUser = null;
	private ContactList contactList = null;
	
	// These are default values and they will be overridden by values in host.properties
	private String host = "localhost";
	private int port = 3141;
	
	@Override
	public synchronized void setActiveUser(IUser activeUser) {
		this.activeUser = activeUser;
	}

	@Override
	public synchronized IUser getActiveUser() {
		return activeUser;
	}

	@Override
	public synchronized void setContactList(ContactList contactList) {
		this.contactList = contactList;
	}

	@Override
	public synchronized ContactList getContactList() {
		return contactList;
	}

	/**
	 * Creates a ChatModel with the provided implementations of sub-systems.
	 * @param serverConn the implementation for the network sub-system.
	 * @param configurator the implementation for the file-IO sub-system.
	 */
	public ChatModel(IServerConnection serverConn, IConfigurator configurator) {
		setServerConnection(serverConn);
		setConfigurator(configurator);
		
		pcs = new PropertyChangeSupport(this);
		loadHostFromFile();
	}

	private void loadHostFromFile() {
		try {
			Map<String, String> data = configurator.load("host.properties");
			if (data.containsKey("host")) {
				host = data.get("host");
			}
			if (data.containsKey("port")) {
				port = Integer.parseInt(data.get("port"));
			}
		} catch (IOException e) {
			System.out.println("File host.properties not found. Using default values.");
		} catch (NumberFormatException e) {
			System.out.println("The port was not a number. Using default port.");
		}
	}

	/**
	 * Sets the network subsystem implementation and adds this object as listener.
	 * @param serverConn the implementation of IServerConnection that should be used.
	 */
	private void setServerConnection(final IServerConnection serverConn) {
		if(serverConn != null) {
			this.serverConn = serverConn;
			this.serverConn.addPropertyChangeListener(this);
		} else {
			this.serverConn = null;
		}
	}

	/**
	 * Sets the file-IO subsystem implementation.
	 * @param configurator the implementation of IConfigurator that should be used.
	 */
	private void setConfigurator(final IConfigurator configurator) {
		this.configurator = configurator;
	}
	
	@Override
	public void connect(final NetworkMessage nm) {
		Runnable connect = new Runnable(){ 
			@Override
			public void run(){
				long startTime = System.currentTimeMillis();
				boolean connecting = false;

				while(!serverConn.isConnected()){
					try {
						long currentTime = System.currentTimeMillis();
						if(currentTime - startTime >= 3000){
							break;
						}
						
						if(!connecting){
							serverConn.connect(5000, host, port);
							connecting = true;
						}
					} catch (IOException e) {}
				}
				
				//Connected, send login credentials
				send(nm);
			}
		};
		
		Thread connecting = new Thread(connect);
		connecting.start();
	}
	
	@Override
	public void addPropertyChangeListener(PropertyChangeListener pcl) {
		pcs.addPropertyChangeListener(pcl);
	}
	
	@Override
	public void removePropertyChangeListener(PropertyChangeListener pcl) {
		pcs.removePropertyChangeListener(pcl);
	}

	@Override
	public void send(NetworkMessage o){
		if(serverConn.isConnected()) {
			serverConn.send(o);
		} else {
			pcs.firePropertyChange("Network Error Message", null, new SystemMessage("The server connection is down, try again later.", SystemMessage.Type.NETWORK_UNAVAILABLE));
		}
	}
	
	@Override
	public void send(SystemMessage systemMessage) {
		pcs.firePropertyChange("System Message", null, systemMessage);
	}
	
	
	/**
	 * Called when something is received from the server.
	 * @param evt
	 */
	@Override
	public void propertyChange(PropertyChangeEvent evt) {		
		if (evt.getNewValue() instanceof NetworkMessage){
			NetworkMessage nMsg = (NetworkMessage)evt.getNewValue();
			System.out.println(nMsg.getObject());
			switch (nMsg.getType()) {
			case LOGIN_SUCCESS:
				System.out.println("LOGIN_SUCCESS");
				loginSuccess(nMsg);
				break;
			case LOGIN_FAIL:
				System.out.println("LOGIN_FAIL");
				disconnect();
				break;
			case SEARCH_RESULTS:
				System.out.println("SEARCH_RESULTS");
				processResults((UserList)nMsg.getObject());
				break;
			case NEW_CONTACT_LIST:
				System.out.println("NEW_CONTACT_LIST");
				setContactList((ContactList) nMsg.getObject());
				break;
			case DISCONNECTED:
				System.out.println("DISCONNECTED");
				disconnect();
				break;
			case MESSAGE:
				IMessage msg = (IMessage) nMsg.getObject();
				processMessage(msg);
				break;
			case STATUS_UPDATE:
				IUser user = (IUser) nMsg.getObject();
				processStatsUpdate(user);
				System.out.println(user.getEmail() + " " + user.isOnline());
				break;
			}
			
			pcs.firePropertyChange(evt);
		}
		else {
			// Should never happen.
		}
	}

	private void processStatsUpdate(IUser user){
		if(this.contactList != null){
			for (int i = 0; i < this.contactList.size(); i++) {
				if(this.contactList.get(i).getUser().equals(user)){
					this.contactList.get(i).getUser().setOnline(user.isOnline());
				}
			}
			
			pcs.firePropertyChange("", null, new NetworkMessage(this.contactList, NetworkMessage.Type.NEW_CONTACT_LIST));
		}
	}
	
	private void processResults(UserList users){
		SearchResults results = new SearchResults(users, this.contactList);
		pcs.firePropertyChange("", null, new NetworkMessage(results, NetworkMessage.Type.SEARCH_RESULTS));
	}

	/**
	 * Checks whether the received message was sent by the IUser set as active user. 
	 * If so the IMessage's setMessageByMe is called with the appropriate flag.
	 * @param msg
	 */
	private void processMessage(IMessage msg) {
		if(msg.getFromUser().equals(this.getActiveUser()) ) {
			msg.setMessageByMe(true);
		} else {
			msg.setMessageByMe(false);
		}
	}
	
	private void loginSuccess(NetworkMessage nm) {
		this.setActiveUser((IUser)nm.getObject());
	}
	
	private void disconnect() {
		setActiveUser(null);
		contactList = null;
		serverConn.disconnect();
	}

	@Override
	public void logout() {
		disconnect();
		send(new SystemMessage(null, SystemMessage.Type.LOGOUT));
	}
	
	@Override
	public boolean isReady(){
		return (getActiveUser() != null) && (getContactList() != null);
	}
	
	@Override
	public void saveLoginSettings(LoginSettings settings) {
		Map<String,String> data = new HashMap<String,String>();
		if (settings.getEmail() != null)
			data.put("email", settings.getEmail());
		if (settings.getPassword() != null)
			data.put("password", settings.getPassword());
		if (settings.getAutoLogin())
			data.put("autoLogin", "true");

		try {
			configurator.save(data, "login.properties");
			System.out.println("Login settings file created.");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public void loadLoginSettings() {
		try {
			Map<String,String> data = configurator.load("login.properties");
			LoginSettings loginSettings = new LoginSettings(
					(data.containsKey("email")) ? data.get("email") : null,
					(data.containsKey("password")) ? data.get("password") : null,
					(data.containsKey("autoLogin")) ? Boolean.parseBoolean(data.get("autoLogin")) : false);

			pcs.firePropertyChange(null, null, new SystemMessage(loginSettings, SystemMessage.Type.UPDATE_LOGIN_SETTINGS));
		} catch (IOException e) {
			System.err.println("Non-fatal error: login.properties does'nt exist");
		}
	}

	@Override
	public void editHost(String host) {
		this.host = host;
	}
}
